/**
 * @file Define the ExpressServer class
 * @description This class is used to setup an Web Server with express framework
 * 
 * @author Steve Gao<gaofu@parllay.com>
 */
const path          = require('path');
const domain        = require('domain');
const express       = require('express');
const session       = require('express-session');
const useragent     = require('express-useragent');
const bodyParser    = require('body-parser');
const RedisStore    = require('connect-redis')(session);
const responseTime  = require('response-time');
const compress      = require('compression');
const uuid          = require('uuid');
const multer        = require('multer');
const i18n          = require('i18next');
const i18nMiddleware = require('i18next-express-middleware');
const i18nFSBackend = require('i18next-node-fs-backend');

const BaseServer        = require('./base-server');
const WebBaseController = require('../controllers/web/web-base-controller');
const BaseHandler       = require('../handlers/web/base-handler');
const SystemServices    = require('../constants').SystemServices;
const Configuration     = require('../constants').Configuration;
const HTTP_VERBS        = require('../constants').HTTP_VERBS;
const HTTP_STATUSES     = require('../constants').HTTP_STATUSES;
const ApplicationEvents = require('../constants').ApplicationEvents;
const CriticalErrors    = require('../constants').CriticalErrors;
const WebRequestError   = require('../errors').WebRequestError;
const utils             = require('../utils');

/**
 * The ExpressServer class
 * 
 * @class ExpressServer
 * @param {Application} application - The OnWall Application
 */
class ExpressServer extends BaseServer {
    
    get logger() {
        if (!this._logger) {
            this._logger = this.Application.Context.getSystemService(SystemServices.LOG_SERVICE).getLogger();
        }

        return this._logger;
    }
    
    get Server() {
        if (!this._Server) {
            this._Server = express();
        }

        return this._Server;
    }

    getExpress() {
        return this.Server;
    }

    get ServerConfig() {
        return this.Application.getConfiguration(Configuration.SERVER);
    }

    get IsUnderMaintainancing() {
        return this.Application.IsUnderMaintainancing;
    }

    constructor(application) {
        super(application);
        this.extendExpress();
        this.initializeServer();
    }

    extendExpress() {
        if (this.Application.Configuration.enable_administration) {
            Object.defineProperty(this.Server, 'Routers', {
                get: function() {
                    try {
                        return this._router.stack;
                    } catch (e) {
                        //TODO: Add log here
                        return [];
                    }
                }
            });
        }
    }

    /**
     * Initialize the express Server
     * 
     * @name initializeServer 
     * 
     * @return {void}
     */
    initializeServer() {
        this._setupBaseOptions();
        this._setupContentParseOptions();
        this._setupOptimizationOptions();
        this._setupSessionOptions();
        this._setupI18NOptions();
        this._setupSecurityOptions();
    }

    /**
     * Setup the express base options
     */
    _setupBaseOptions() {
        var me = this;
        this.Server.set('view engine', this.ServerConfig.express.view_engine);
        this.Server.set('etag', this.ServerConfig.express.etag);
        this.Server.use((request, response, next) => {
            // Add some temporary logic here to 
            // redirect permanantly to our server when 
            // some third party domain name are mapped 
            // to our host
            //TODO: We need add some more powerfully logic here.
            let host = request.headers['host'];
            response.setHeader('X-Powered-By', me.Application.name);
            next();
        });

        // isolate all error
        this.Server.use((request, response, next) => {
            var isolator = domain.create();
            isolator.once('error', (error) => {
                isolator.emit('cleanup');
                me.logger.error(`Failed to handle request [${request.url}] `, error);
                request.requestData.error = {
                    message: error.message,
                    stack: error.stack
                };
                request.end_time = new Date();
                me.reportError(ApplicationEvents.WEB_REQUEST_ERROR, 
                    new WebRequestError(`Failed to handle request [${request.url}], error happened:`, 
                                        CriticalErrors.WEB_REQUEST_ERROR, 
                                        request.requestData));
                response.status(HTTP_STATUSES.INTERNAL_ERROR).render('common/internal-error');
            });

            isolator.add(request);
            isolator.add(response);
            //isolator.enter();
            isolator.run(() => {
                next();
            });
        });

        //Parse User-Agent header into request.useragent
        this.Server.use(useragent.express());

        // Initialize request data
        this.Server.use((request, response, next) => {
            request.requestData = {
                id: `request-${utils.generateIdentifier(20, true)}`,
                start_time: new Date()
            };

            next();
        });
    }
    
    _setupSessionOptions() {
        var me = this;
        var sessionConfig = this.Application.getConfiguration(Configuration.SESSION);
        var redisConfig = this.Application.getConfiguration(Configuration.REDIS);
        var sessionStoreOptions = {
            disableTTL: true,
            prefix: sessionConfig.prefix,
            host: redisConfig.host,
            port: redisConfig.port
        };
        if(redisConfig.password) {
            sessionStoreOptions.pass = redisConfig.password;
        }

        this.Server.use(session({
            name: sessionConfig.name,
            genid: function(req) {
                return uuid.v4();
            },
            cookie: {
                maxAge: sessionConfig.age
            },
            resave: true,
            saveUninitialized: false,
            store: new RedisStore(sessionStoreOptions),
            secret: sessionConfig.secret
        }));

        this.Server.use((request, response, next) => {
            if(request.session && !request.session.user) {
                request.session.user = {
                    authenticated: false
                };
            }
            var userAgent = request.headers['user-agent'] || '';
            request.session.is_wechat = false;
            if(userAgent.indexOf(me.ServerConfig.express.wechat_detect_code) >= 0) {
                request.session.is_wechat = true;
            }
            
            request.is_ajax = (request.headers['x-requested-with'] === 'XMLHttpRequest');

            if(request.session && !request.session.walls) {
                request.session.walls = {};
            }
            
            next();
        });
    }

    _setupI18NOptions() {
        i18n.use(i18nMiddleware.LanguageDetector)
            .use(i18nFSBackend)
            .init({
                detection: {
                    order: ['session', 'cookie', 'header'],
                    lookupSession: 'locale',
                    lookupCookie: 'locale'
                },
                fallbackLng: 'en',
                initImmediate: true,
                backend: {
                    loadPath: path.resolve(this.Application.Context.getCurrentWorkDirectory(), "static/locales/{{lng}}/resources.json")
                }
            });

        this.Server.use(i18nMiddleware.handle(i18n));
    }

    _setupOptimizationOptions() {
        if(this.ServerConfig.express.enable_response_time) {
            this.Server.use(responseTime());
        }

        if(this.ServerConfig.express.enable_compress) {
            this.Server.use(compress());
        }
    }

    _setupContentParseOptions() {
        var staticPath = path.resolve(this.Application.Context.getCurrentWorkDirectory(), this.ServerConfig.express.static_path);
        this.Server.use(express.static(staticPath));
        this.Server.use(bodyParser.text({
            type: 'text/xml'
        }));
        this.Server.use(bodyParser({limit: '50mb'}));
        this.Server.use(bodyParser.urlencoded({
            extended: true,
            limit: 1024 * 1024 * 20, //Move to configuration
            type: 'application/x-www-form-urlencoding'
        }));
        this.Server.use(bodyParser.json({
            limit: 1024 * 1024 * 20, //Move to configuration
            type:'application/json'
        }));
        this.Server.use(bodyParser());
        this.Server.use(multer({limits: {
                fileSize: 20 * 1024 * 1024 // Move to configuration
            }
        }).any());
    }

    _setupSecurityOptions() {
        
    }

    get controllersMap () {
        if(!this._controllersMap) {
            this._controllersMap = new Map();
        }

        return this._controllersMap;
    }

    get ControllerClasses() {
        if ( ! this._controllerClasses ) {
            this._controllerClasses = [];
        }

        return this._controllerClasses;
    }

    /**
     * Register web request controller into Server 
     * 
     * @name registerWebController
     * @param {Class} controllerClass - The controller class
     */
    registerWebController(controllerClass) {
        var className = controllerClass.name ? controllerClass.name : "Unknow Controller";
        this.logger.info(`Begin to register [${className}] into web server ...`)
        var controllerInstance = new controllerClass(this.Application, this);
        if(controllerInstance && controllerInstance instanceof WebBaseController) {
            this.ControllerClasses.push({
                name: controllerInstance.name,
                clazz: controllerClass
            });
            this.controllersMap.set(controllerInstance.name, controllerInstance);
            this.logger.info(`Finsihed registering [${className}] into web server ...`);
            return true;
        }

        this.logger.info(`Failed to register [${className}] into web server ...`);
        return false;
    }

    get handlersMap() {
        if (!this._handlersMap) {
            this._handlersMap = new Map();
        }

        return this._handlersMap;
    }

    /**
     * Register request handler into Express Server
     * 
     * @name registerRequestHandler
     * @param {String} name - The name of the handler
     * @param {String} handlerFile - The real path of the handler
     */
    registerRequestHandler(controller, name, handlerFile) {
        var handlerClass = require(handlerFile);
        var handlerInstance = null;
        try {
            this.logger.info(` - [${name}]: ${handlerFile}`);
            handlerInstance = new handlerClass(controller, this, this.Application);
        } catch (e) {
            this.logger.error(`Failed to load handler(${name}) into web server`, e);
        }
        if (handlerInstance && handlerInstance instanceof BaseHandler) {
            this.handlersMap.set(name, handlerInstance);
            handlerInstance.setup();
        }
    }

    /**
     * Register the handler's verb handler into Express Routes
     * 
     * @name registerRequestVerbHandler
     * @param {BaseHandler} - The handler class
     * @param {String} verb - The HTTP verb
     * @param {Function} - The handle function for HTTP request
     * 
     * @returns {Boolean}
     * @memberOf ExpressServer
     */
    registerRequestVerbHandler(handler, verb, handleFunction) {
        var me = this;
        if (verb === HTTP_VERBS.ERROR) {
            this.Server.use((error, request, response, next) => {
                handler[handleFunction.name](error, request, response, next);
            });
        } else {
            this.Server[verb](handler.pattern, (request, response) => {
                var requestId = request.requestData.id;
                var startTime = request.requestData.start_time;

                request.requestData.url         = request.url;
                request.requestData.headers     = request.headers;
                request.requestData.method      = request.method;
                request.requestData.params      = request.params;
                request.requestData.query       = request.query;
                request.requestData.session     = request.session;
                request.requestData.body        = request.body;
                request.requestData.sessionID   = request.session.id;

                me.logger.info(` ${request.method} - ${request.url} => [${requestId}] Started at [${startTime}]`);
                response.on('finish', () => {
                    if( response.finished ) {
                        var endTime = new Date();
                        var spentTime = `${(endTime - startTime) / 1000} seconds`;
                        request.requestData.end_time = endTime;
                        //TODO: Send them into kafka, if error, send mail to developers
                        //console.log(JSON.stringify(request.requestData));
                        me.logger.info(` ${request.method} - ${request.url} => [${requestId}] Finished at [${endTime}] Spent [${spentTime}]`);
                    }
                });
                if (me.IsUnderMaintainancing) {
                    // Add some metric counter logic here
                    me.renderUnderMaintainancingPage(response);
                } else {
                    handler[handleFunction.name](request, response);
                }
            });
        }

        return true;
    }

    /**
     * Render the maintainance page when the Application is in maintainance status
     * 
     * @name renderUnderMaintainancingPage
     * @param {HttpResponse} response
     * 
     * @returns {void}
     * 
     * @memberOf ExpressServer
     */
    renderUnderMaintainancingPage(response) {
        response.status(HTTP_STATUSES.SERVER_UNAVAILABLE).render('common/maintainance');
    }

    /**
     * To refresh all controllers 
     * 
     * @name refreshControllers
     * 
     * @memberOf ExpressServer
     */
    refreshControllers() {
        this.cleanRequestHandlers();
    }

    cleanControllers() {

    }

    cleanRequestHandlers() {
        for( var i = 0; i < this.Server.Routers.length; i ++ ) {
            if ( this.Server.Routers[i].name === 'bound dispatch' ) {
                this.Server.Routers.splice(i, 1);
                i --;
            }   
        }
    }

    reportError(errorName, error) {
        this.Application.reportError(errorName, error);
    }
    
    /**
     * Start the express server
     * 
     * @name start
     */
    start(callback) {
        utils.checkFunctionParameter(callback, 'callback');
        var me = this;
        this.Server.listen(this.ServerConfig.port, () => {
            me.logger.info(`${me.Application.name} has listened at 0.0.0.0:${me.ServerConfig.port}.`);
            callback();
        });
    }
    

    stop() {
        
    }
}

exports.ExpressServer = ExpressServer;